package pokerserver.core;

import java.util.Arrays;

import pokerserver.core.Hand.HandCategory;

import common.Card;
import common.Card.Rank;
import common.Card.Suit;

/**
 * A class which takes care of the evaluation of the players' hands, and
 * determines which hand is the winning hand.
 * 
 */
public class HandComparator {
	// A two-dimensional array that contains a reference to the cards relevant
	// to a player's hand's category in the second array and a reference to the
	// player in the first:
	private static int[][] relevantCardRange;
	private static int currentPlayerIndex;

	private static final Rank[] ranks = { Rank.ACE, Rank.TWO, Rank.THREE,
			Rank.FOUR, Rank.FIVE, Rank.SIX, Rank.SEVEN, Rank.EIGHT, Rank.NINE,
			Rank.TEN, Rank.JACK, Rank.QUEEN, Rank.KING, Rank.ACE };

	// The only public class. It takes in an array of Players and returns an
	// array of Players. The difference between these two is that the return
	// array contains only the winning players:
	public static Player[] determineWinner(Player[] playerArray,
			Card[] communityCards) {
		relevantCardRange = new int[4][5];

		// Set all numbers to -1 so that we can tell which ones haven't been
		// manipulated later.
		for (int i = 0; i < 4; i++) {
			for (int j = 0; j < 2; j++) {
				relevantCardRange[i][j] = -1;
			}
		}

		// Evaluate each hand:
		for (int i = 0; i < 4; i++) {
			Player p = playerArray[i];
			if (p != null) {
				Hand playerHand = p.getHand();
				playerHand.setCommunityCards(communityCards);
				currentPlayerIndex = i;
				evaluateHand(playerHand);
			}
		}

		// Determine scores as such:
		int[] scores = new int[4];
		int highestScore = 0;
		for (int i = 0; i < 4; i++) {
			if (playerArray[i] != null) {
				HandCategory category = playerArray[i].getHand().getCategory();
				scores[i] = getHandCategoryScore(category);
				if (scores[i] > highestScore) {
					highestScore = scores[i];
				}
			} else {
				scores[i] = 0;
			}
		}
		// Create an array with winners. If there is only one by this stage, we
		// will return it.
		Player[] prospectWinners = new Player[4];
		int winnerCounter = 0;
		for (int i = 0; i < 4; i++) {
			if (scores[i] == highestScore) {
				prospectWinners[i] = playerArray[i];
				winnerCounter++;
			} else {
				prospectWinners[i] = null;
			}
		}

		if (winnerCounter == 1) {
			return prospectWinners;
		} else {
			return getKickerWinner(prospectWinners);
		}

	}

	// Run for each player by the determineWinner method:
	private static void evaluateHand(Hand hand) {
		// We want all 7 cards:
		Card[] allCards = hand.getAllCards();
		Arrays.sort(allCards);

		// An array of Integers which keeps track on how many occurrences of
		// each type of rank there is in the allCards array:
		int[] numberOfEachRank = new int[14];

		// Populate the array, skip the first ace (as aces count both high and
		// low they exist twice in the array of ranks)
		for (int i = 0; i < 13; i++) {
			for (Card c : allCards) {
				if (ranks[i + 1] == c.getRank()) {
					numberOfEachRank[i]++;
				}
			}
		}

		// Integers representing the number of occurrences of [suit] in this
		// hand
		int spades = 0, hearts = 0, clubs = 0, diamonds = 0;

		// Count how many times each suit appears
		for (Card c : allCards) {
			Suit suit = c.getSuit();
			if (suit == Suit.SPADES) {
				spades++;
			} else if (suit == Suit.HEARTS) {
				hearts++;
			} else if (suit == Suit.CLUBS) {
				clubs++;
			} else {
				diamonds++;
			}
		}

		// Time to perform some fast checks. These are the least common
		// occurrences, so most of these if statements will simply be false and
		// therefore gone through very quickly.
		if (spades > 4 || hearts > 4 || clubs > 4 || diamonds > 4) {
			Suit suit;
			if (spades > 4) {
				suit = Suit.SPADES;
			} else if (hearts > 4) {
				suit = Suit.HEARTS;
			} else if (clubs > 4) {
				suit = Suit.CLUBS;
			} else {
				suit = Suit.DIAMONDS;
			}
			// We have a flush! Could it be a straight flush?
			if (isStraightFlush(hand)) {
				// Congratulations!
				hand.setCategory(HandCategory.STRAIGHT_FLUSH);
				return;
			} else {
				if (isQuads(numberOfEachRank)) {
					hand.setCategory(HandCategory.FOUR_OF_A_KIND);
					return;
				} else if (isFullHouse(numberOfEachRank)) {
					hand.setCategory(HandCategory.FULL_HOUSE);
					return;
				} else {
					// We have to add the RCR here, as there is no "isFlush"
					// method
					// to handle this:
					registerFlushRCR(hand, suit);
					hand.setCategory(HandCategory.FLUSH);
					return;
				}
			}
		}

		if (isStraight(hand)) {
			hand.setCategory(HandCategory.STRAIGHT);
			return;
		}

		if (isQuads(numberOfEachRank)) {
			hand.setCategory(HandCategory.FOUR_OF_A_KIND);
			return;
		}

		if (isTrips(numberOfEachRank)) {
			// Could it be a full house?
			if (isFullHouse(numberOfEachRank)) {
				hand.setCategory(HandCategory.FULL_HOUSE);
				return;
			} else {
				hand.setCategory(HandCategory.THREE_OF_A_KIND);
				return;
			}
		}

		if (isPair(numberOfEachRank)) {
			// Could it be a two-pair?
			if (isTwoPair(numberOfEachRank)) {
				hand.setCategory(HandCategory.TWO_PAIR);
				return;
			} else {
				hand.setCategory(HandCategory.ONE_PAIR);
				return;
			}
		} else {
			hand.setCategory(HandCategory.HIGH_CARD);
			// We have to add the RCR here, as there is no "isHighCard" method
			// to handle this:
			for (int i = 0; i < numberOfEachRank.length; i++) {
				int number = numberOfEachRank[i];
				int numberCounter = 0;
				if (number > 0) {
					addRankIndexToRCR(i);
					if (numberCounter == 5) {
						break;
					}
				}
			}
			return;
		}
	}

	private static void registerFlushRCR(Hand hand, Suit suit) {
		Card[] allCards = hand.getAllCards();

		for (int i = allCards.length - 1; i >= 0; i--) {
			Card card = allCards[i];
			if (card.getSuit() == suit) {
				addRankIndexToRCR(card.getRank().ordinal());
			}
		}

	}

	/**
	 * Check if the hand is a Straight Flush
	 * 
	 * @param hand
	 *            the hand to evaluate
	 * @return true if straightflush, else false
	 */
	private static boolean isStraightFlush(Hand hand) {
		Card[] allCards = hand.getAllCards();
		Arrays.sort(allCards);

		int straightCounter = 0;
		Suit lastSuit = allCards[0].getSuit();
		Rank lastRank = allCards[0].getRank();
		for (int i = 1; i < allCards.length; i++) {
			Card thisCard = allCards[i];
			Suit thisSuit = thisCard.getSuit();
			Rank thisRank = thisCard.getRank();
			if (thisSuit == lastSuit) {
				if (thisRank != lastRank) {
					straightCounter++;
				}
			} else {
				straightCounter = 0;
			}

			if (straightCounter == 5) {
				return true;
			}
			lastSuit = thisSuit;
			lastRank = thisRank;
		}

		// We could not find a straight flush:
		return false;
	}

	/**
	 * Checks if the hand is a twoPair.
	 * 
	 * @param numberOfEachRank
	 *            all ranks in the hand
	 * @return true/false if two pair or not
	 */
	private static boolean isTwoPair(int[] numberOfEachRank) {
		// Count the number of pairs in our array (represented by a number of i
		// larger than 1)
		int pairsFound = 0;
		int firstPairRank = -1;
		int secndPairRank = -1;
		for (int i = numberOfEachRank.length - 1; i >= 0; i--) {
			int number = numberOfEachRank[i];
			if (number > 1) {
				pairsFound++;
				if (firstPairRank == -1) {
					firstPairRank = i;
				} else {
					secndPairRank = i;
				}
			}
		}

		if (pairsFound > 1) {
			addRankIndexToRCR(firstPairRank, 2);
			addRankIndexToRCR(secndPairRank, 2);
			// Find the "kicker"
			for (int i = numberOfEachRank.length - 1; i >= 0; i--) {
				int number = numberOfEachRank[i];
				if (number > 0 && i != firstPairRank && i != secndPairRank) {
					addRankIndexToRCR(i);
					// Only do this for a single kicker:
					break;
				}
			}
			return true;
		} else {
			return false;
		}
	}

	/**
	 * Checks if the hand is a pair.
	 * 
	 * @param numberOfEachRank
	 *            array of each rank in the hand
	 * @return if the hand has a pair or not (true/false)
	 */
	private static boolean isPair(int[] numberOfEachRank) {
		int pairRank = -1;
		for (int i = numberOfEachRank.length - 1; i >= 0; i--) {
			int number = numberOfEachRank[i];
			if (number > 1) {
				pairRank = i;

				break;
			}
		}

		// If pairRank isn't -1, we have a pair.
		if (pairRank != -1) {
			addRankIndexToRCR(pairRank, 2);
			// Find the "kicker"
			int kickerCounter = 0;
			for (int i = numberOfEachRank.length - 1; i >= 0; i--) {
				int number = numberOfEachRank[i];
				if (number > 0 && i != pairRank) {
					addRankIndexToRCR(i);
					kickerCounter++;
					if (kickerCounter > 2) {
						break;
					}
				}
			}
			return true;
		} else {
			clearCurrentRCREntry();
			return false;
		}
	}

	/**
	 * Check if the hand is a quads.
	 * 
	 * @param numberOfEachRank
	 *            an array of all ranks in the hand
	 * @return if the hand is a quads (true/false)
	 */
	private static boolean isQuads(int[] numberOfEachRank) {
		// Fairly self-explanatory:
		int quadsRank = -1;
		for (int i = numberOfEachRank.length - 1; i >= 0; i--) {
			int number = numberOfEachRank[i];
			if (number == 4) {
				quadsRank = i;
				addRankIndexToRCR(quadsRank, 4);
			}
		}

		// Find the "kicker"
		if (quadsRank != -1) {
			for (int i = numberOfEachRank.length - 1; i >= 0; i--) {
				int number = numberOfEachRank[i];
				// Make sure we're not dealing with the same number as the
				// quadsRank:
				if (number > 0 && i != quadsRank) {
					addRankIndexToRCR(i);
					// Only add a single kicker:
					break;
				}
			}
			return true;
		} else {
			// We have gone through the entire array and we did not find
			// four of a single rank:
			clearCurrentRCREntry();
			return false;
		}
	}

	/**
	 * Checks if the hand is a straight
	 * 
	 * @param hand
	 *            the hand to evaluate
	 * @return if the hand is a straight or not (true/false)
	 */
	private static boolean isStraight(Hand hand) {
		Card[] allCards = hand.getAllCards();
		int[] numberOfEachRank = new int[14];

		// Create a new list of ranks, this time also counting the first ace in
		// the ranks array
		for (int i = 0; i < 14; i++) {
			for (Card c : allCards) {
				if (ranks[i] == c.getRank()) {
					numberOfEachRank[i]++;
				}
			}
		}

		// Fairly self-explanatory:
		int streakCounter = 0;
		int firstStraightEntry = -1;
		for (int i = 0; i < numberOfEachRank.length; i++) {
			int number = numberOfEachRank[i];
			if (number > 0) {
				streakCounter++;
			} else {
				streakCounter = 0;
			}
			// (We need more than 4 ranks in a row for a straight)
			if (streakCounter > 4) {
				firstStraightEntry = i - 4;
				for (int j = firstStraightEntry; j < firstStraightEntry + 5; j++) {
					// Add the five card ranks to the RCR:
					addRankIndexToRCR(j);
				}
				return true;
			}
		}
		// Nothing was found. Go home!
		clearCurrentRCREntry();
		return false;
	}

	/**
	 * If the hand contains a trips
	 * 
	 * @param numberOfEachRank
	 *            array or the ranks (as integers) in the hand
	 * @return if the hand has a trips (true/false)
	 */
	private static boolean isTrips(int[] numberOfEachRank) {
		// Fairly self-explanatory:
		int tripsRank = -1;
		for (int i = numberOfEachRank.length - 1; i >= 0; i--) {
			int number = numberOfEachRank[i];
			if (number > 2) {
				tripsRank = i;
				break;
			}
		}

		int kickerCounter = 0;
		if (tripsRank != -1) {
			addRankIndexToRCR(tripsRank, 3);

			// Find the two kickers:
			for (int i = numberOfEachRank.length - 1; i >= 0; i--) {
				int number = numberOfEachRank[i];
				if (number > 0 && i != tripsRank) {
					addRankIndexToRCR(i);
					kickerCounter++;
					if (kickerCounter > 1) {
						return true;
					}
				}
			}
		}
		// We have gone through the entire array and we did not find
		// three of a single rank:
		clearCurrentRCREntry();
		return false;

	}

	/**
	 * Check for full house
	 * 
	 * @param numberOfEachRank
	 *            integers representing ranks
	 * @return if full house (true/false)
	 */
	private static boolean isFullHouse(int[] numberOfEachRank) {
		int tripsRank = -1, pairRank = -1;
		for (int i = numberOfEachRank.length - 1; i >= 0; i--) {
			int number = numberOfEachRank[i];
			if (number == 3) {
				tripsRank = i;
			} else if (number == 2) {
				pairRank = i;
			}
		}
		if (tripsRank != -1 && pairRank != -1) {
			addRankIndexToRCR(tripsRank, 3);
			addRankIndexToRCR(pairRank, 2);
			return true;
		} else {
			// We have gone through all cards for each rank. We could not find a
			// full house.
			return false;
		}
	}

	/**
	 * Static method. Returns which score a category of hand gives.
	 * 
	 * @param category
	 *            the category you want the score for
	 * @return the score
	 */
	private static int getHandCategoryScore(HandCategory category) {
		if (category == HandCategory.HIGH_CARD) {
			return 1;
		} else if (category == HandCategory.ONE_PAIR) {
			return 2;
		} else if (category == HandCategory.TWO_PAIR) {
			return 3;
		} else if (category == HandCategory.THREE_OF_A_KIND) {
			return 5;
		} else if (category == HandCategory.STRAIGHT) {
			return 7;
		} else if (category == HandCategory.FLUSH) {
			return 11;
		} else if (category == HandCategory.FULL_HOUSE) {
			return 13;
		} else if (category == HandCategory.FOUR_OF_A_KIND) {
			return 17;
		} else if (category == HandCategory.STRAIGHT_FLUSH) {
			return 19;
		} else {
			return 0;
		}
	}

	// Returns what player(s) (has/have) the highest ranking card
	private static Player[] getKickerWinner(Player[] playerArray) {
		Player[] winners = new Player[4];

		// Create a "score board" using the comparePlayerHands. We will use this
		// method to determine the winner(s) by playing each player against
		// every other player and giving hands depending on how well they fare.
		int[] scoreBoard = new int[4];
		for (int i = 0; i < 4; i++) {
			if (playerArray[i] != null) {
				for (int j = 0; j < 4; j++) {
					if (playerArray[j] != null) {
						// Don't play against yourself, that'd just result in a
						// draw
						if (i != j) {
							scoreBoard[i] += comparePlayerHands(i, j);
						}
					}
				}
			}
		}

		// Find the highest score in the "score board":
		int highestScore = 0;
		for (int score : scoreBoard) {
			if (score > highestScore) {
				highestScore = score;
			}
		}

		// Add all players in the initial playerArray to the array with winners
		// if they have the highest score (i.e. the winning score).
		for (int i = 0; i < 4; i++) {
			if (scoreBoard[i] == highestScore) {
				winners[i] = playerArray[i];
			}
		}

		return winners;
	}

	/**
	 * Compares two hands in the RCR (relevant card range). Takes in the
	 * position of two players whose hands are to be compared. Returns the
	 * "score" that player 1 gets from this most epic of battles. If player 1
	 * has a worse hand than player 2, the score is zero. If the hands are equal
	 * the score is one and if player 1's hand is better, the score is two.
	 * 
	 * @param player1
	 *            position of first player
	 * @param player2
	 *            position of second player
	 * @return 0, 1 or 2. 0 = player1 lost, 1 = tied, 2 = player 2 lost
	 */
	private static int comparePlayerHands(int player1, int player2) {
		int[] hand1 = relevantCardRange[player1];
		int[] hand2 = relevantCardRange[player2];

		for (int i = 0; i < 5; i++) {
			if (hand1[i] < hand2[i]) {
				return 0;
			} else if (hand1[i] > hand2[i]) {
				return 2;
			} else if (hand1[i] == hand2[i]) {
				// Do nothing.
			}
		}
		// We went through the entire loop without finding any difference
		// between the two hands:
		return 1;
	}

	/**
	 * Adds a index of a rank to the relevant card range
	 * @param cardRankIndex index to add.
	 */
	private static void addRankIndexToRCR(int cardRankIndex) {
		for (int i = 0; i < 5; i++) {
			if (relevantCardRange[currentPlayerIndex][i] == -1) {
				relevantCardRange[currentPlayerIndex][i] = cardRankIndex;
				return;
			}
		}
	}

	/**
	 *  Add a single index to the RCR several times:
	 * @param cardRankIndex the index
	 * @param times how many times to add
	 */
	private static void addRankIndexToRCR(int cardRankIndex, int times) {
		for (int i = 0; i < times; i++) {
			addRankIndexToRCR(cardRankIndex);
		}
	}

	/**
	 * Clears the current RCR Entry
	 */
	private static void clearCurrentRCREntry() {
		for (int i = 0; i < 5; i++) {
			relevantCardRange[currentPlayerIndex][i] = -1;
		}
	}
}
